module HistoryRecord
  CREATE = "C"
  UPDATE = "U"
  DESTROY = "D"
  ADD_ASSOCIATION = "A"
  REMOVE_ASSOCIATION = "R"

  def self.included(base)
    base.extend(ClassMethods)

    base.send :include, HistoryRecord::InstanceMethods

    base.after_create do |model|
      model.log_create if model.class.has_history_table?
    end
    base.before_update do |model|
      model.log_update if model.class.has_history_table?
    end
    base.before_destroy do |model|
        model.log_destroy if model.class.has_history_table?
    end
    base.before_validation do |model|
      model.class.append_habtm_callbacks if model.class.has_history_table?
    end
  end

  module ClassMethods
    def append_habtm_callbacks
      reflect_on_all_associations.each do |association|
        if association.macro == :has_and_belongs_to_many
          append_add_callback(association)
          append_remove_callback(association)
        end
      end
    end

    def append_add_callback(association)
      add_callback = "before_add_for_#{association.name}"
      return unless read_inheritable_attribute(add_callback.to_sym).nil?
      class_inheritable_reader(add_callback.to_sym)
      write_inheritable_array(add_callback.to_sym, [:log_add])
    end

    def append_remove_callback(association)
      remove_callback = "before_remove_for_#{association.name}"
      return unless read_inheritable_attribute(remove_callback.to_sym).nil?
      class_inheritable_reader(remove_callback.to_sym)
      write_inheritable_array(remove_callback.to_sym, [:log_remove])
    end

    def has_history_table?
      connection.tables.include?("#{self.table_name}_changes")
    end

    def get_history_table
      return get_history_class.new
    end

    def get_history_class
      return get_history_class_by_table_name("#{self.table_name}_changes")
    end

    def get_history_table_by_table_name(history_table_name)
      return get_history_class_by_table_name(history_table_name).new
    end

    def get_history_class_by_table_name(history_table_name)
      Utils::Dynamic.klass(history_table_name)
    end

  end#end ClassMethods

  module InstanceMethods
    def log_create
      log_change(HistoryRecord::CREATE)
    end

    def log_update
      log_change(HistoryRecord::UPDATE)
    end

    def log_destroy
      log_change(HistoryRecord::DESTROY)
    end

    def log_add(model)
      log_association_change(model,HistoryRecord::ADD_ASSOCIATION) if self.class.has_history_table?
    end

    def log_remove(model)
      log_association_change(model,HistoryRecord::REMOVE_ASSOCIATION) if self.class.has_history_table?
    end

    private
      def log_change(type)
        if type == HistoryRecord::UPDATE
          write_update_log(self.class.find(self.id))
        else
          write_create_or_delete_log(type)
        end
      end

      def log_association_change(model,type)
        if model.respond_to?(:name)
          if type == HistoryRecord::ADD_ASSOCIATION
            save_history_record(type,"#{model.class.name.downcase}.name",
                               nil,model.name.to_s)
          else
            save_history_record(type,"#{model.class.name.downcase}.name",
                               model.name.to_s,nil)
          end
        elsif model.respond_to?(:value)
          if type == HistoryRecord::ADD_ASSOCIATION
            save_history_record(type,"#{model.class.name.downcase}.value",
                               nil,model.value.to_s)
          else
            save_history_record(type,"#{model.class.name.downcase}.value",
                               model.value.to_s,nil)
          end
        else
          if type == HistoryRecord::ADD_ASSOCIATION
            save_history_record(type,"#{model.class.name.downcase}.id",
                               nil,model.id.to_s)
          else
            save_history_record(type,"#{model.class.name.downcase}.id",
                               model.id.to_s,nil)
          end
        end
      end

      def write_update_log(prior)
        self.attributes.each do |key, value|
          if prior.attributes.has_key?(key)
            old_value = prior.attributes.fetch(key)
            if old_value != value and is_loggable_field?(key)
              save_history_record(HistoryRecord::UPDATE,key,old_value,value)
            end
          end
        end
      end

      def write_create_or_delete_log(type)
        self.attributes.each do |key,value|
          if not key =~ /^id$/
            if type == HistoryRecord::CREATE
              save_history_record(type,key,nil,value) if !value.nil? and is_loggable_field?(key)
            else
              save_history_record(type,key,value) if !value.nil? and is_loggable_field?(key)
            end
          end
        end
      end

      def new_value?(key,value)
        self.attributes.fetch(key) != value
      end

      def is_loggable_field?(key)
        #don't log field ending in these values
        not (key =~ /(_at|_by|_on)$/)
      end

      def save_history_record(type, field_name, old_value, new_value = nil)
        bt = self.class.get_history_table
        bt.reference_id = self.reference_id if self.respond_to?(:reference_id)
        bt.record_id = self.id
        bt.change_type = type
        bt.field_name = field_name
        bt.old_value = old_value
        bt.new_value = new_value
        bt.changed_at = DateTime.now
        bt.changed_by = self.updated_by if self.respond_to?(:updated_by)
        bt.save
      rescue Exception => e
        logger.error("Error saving history record:\n #{e}")
        raise "Error while saving history record"
      end

  end #InstanceMethods
end #HistoryRecord

ActiveRecord::Base.send :include, HistoryRecord

